﻿#region NeoPatterns
// 
// NeoPatterns framework is released under LGPLv3 license.
// See http://www.opensource.org/licenses/lgpl-3.0.html for more details.
// 
// Please visit http://neopatterns.wikidot.com for more information about NeoPatterns
// 
// Copyright (c) Pascal Craponne 2008
// 
#endregion

using System;
using System.Reflection;
using NeoPatterns.Exception;

namespace NeoPatterns.Patterns.Prototype
{
    /// <summary>
    /// Prototype implementation by reflection
    /// </summary>
    public class ReflectionPrototyper : IPrototyper
    {
        /// <summary>
        /// This method is called when there is no match on member between source and target type
        /// </summary>
        protected virtual void NoMatch(MemberInfo memberInfo)
        {
            // and does nothing for now.
            // In this implementation we don't really care
        }

        /// <summary>
        /// Copy members from one instance to another, using reflection from a specified type
        /// </summary>
        /// <param name="source"></param>
        /// <param name="target"></param>
        /// <param name="sourceType"></param>
        protected virtual void CopyMembers(object source, object target, Type sourceType)
        {
            var targetType = target.GetType();
            var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic |
                               BindingFlags.FlattenHierarchy;

            // start with field (non-dymanic)
            foreach (var sourceFieldInfo in sourceType.GetFields(bindingFlags))
            {
                var targetFieldInfo = targetType.GetField(sourceFieldInfo.Name, bindingFlags);
                if (targetFieldInfo == null)
                    NoMatch(sourceFieldInfo);
                else
                {
                    var value = sourceFieldInfo.GetValue(source);
                    targetFieldInfo.SetValue(target, value);
                }
            }

            // then copy properties (dynamic, based on fields)
            foreach (var sourcePropertyInfo in sourceType.GetProperties(bindingFlags))
            {
                var targetPropertyInfo = targetType.GetProperty(sourcePropertyInfo.Name, bindingFlags);
                if (targetPropertyInfo == null)
                    NoMatch(sourcePropertyInfo);
                else
                {
                    var setMethod = targetPropertyInfo.GetSetMethod();
                    // we set the value only if the property has a setter
                    if (setMethod != null)
                    {
                        var value = sourcePropertyInfo.GetGetMethod().Invoke(source, null);
                        setMethod.Invoke(target, new[] { value });
                    }
                }
            }
        }

        /// <summary>
        /// Clones object, with a type to copy reflection from, and a type to target
        /// </summary>
        /// <param name="prototype"></param>
        /// <param name="prototypeType">Prototype type to copy reflection information from</param>
        /// <param name="targetType"></param>
        /// <returns></returns>
        public object CloneAs(object prototype, Type prototypeType, Type targetType)
        {
            // check we work on something possible
            if (!prototypeType.IsAssignableFrom(prototype.GetType()))
                throw new ConsistencyException();
            var targetInstance = Activator.CreateInstance(targetType);
            CopyMembers(prototype, targetInstance, prototypeType);
            return targetInstance;
        }

        /// <summary>
        /// Clones object, given target type
        /// </summary>
        /// <param name="prototype"></param>
        /// <param name="targetType"></param>
        /// <returns></returns>
        public object CloneAs(object prototype, Type targetType)
        {
            return CloneAs(prototype, prototype.GetType(), targetType);
        }

        /// <summary>
        /// Clone object, given source and target type, generic version
        /// </summary>
        /// <typeparam name="TTarget"></typeparam>
        /// <typeparam name="TSource"></typeparam>
        /// <param name="prototype"></param>
        /// <returns></returns>
        public TTarget CloneAs<TTarget, TSource>(TSource prototype)
        {
            return (TTarget)CloneAs(prototype, typeof(TSource), typeof(TTarget));
        }

        /// <summary>
        /// Clone object, given target type, generic version 
        /// </summary>
        /// <typeparam name="TTarget"></typeparam>
        /// <param name="prototype"></param>
        /// <returns></returns>
        public TTarget CloneAs<TTarget>(object prototype)
        {
            return (TTarget)CloneAs(prototype, typeof(TTarget));
        }

        /// <summary>
        /// Clone object, simple version
        /// </summary>
        /// <typeparam name="TObject"></typeparam>
        /// <param name="prototype"></param>
        /// <returns></returns>
        public TObject Clone<TObject>(TObject prototype)
        {
            var cloneable = prototype as ICloneable;
            if (cloneable != null)
                return (TObject)cloneable.Clone();
            return CloneAs<TObject>(prototype);
        }
    }
}
